#
# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Defines a USB Identifier."""

import re
from typing import Dict, List, NamedTuple, Optional, Pattern, Any, cast

from mbed_tools.devices._internal.windows.component_descriptor_utils import is_undefined_data_object
from mbed_tools.devices._internal.windows.windows_identifier import WindowsUID

KEY_UID = "UID"


class UsbIdentifier(NamedTuple):
    """Object describing the different elements present in the device ID.

    Attributes:
        UID: Universal ID, either the serial number or device instance ID.
        VID: Vendor ID, 4 digit.
        PID: Product ID assigned to the devices, 4 digit.
        REV: Revision code.
        MI: Multiple Interface, a 2 digit interface number.
    """

    UID: Optional[str] = None
    VID: Optional[str] = None
    PID: Optional[str] = None
    REV: Optional[str] = None
    MI: Optional[str] = None

    @staticmethod
    def get_patterns_dict() -> Dict[str, Pattern]:
        """Returns a dictionary of all the regexes."""
        return {p: re.compile(f"^{p}_(.*)$") for p in UsbIdentifier._fields[1:]}

    @property
    def uid(self) -> WindowsUID:
        """Gets the USB ID."""
        return cast(WindowsUID, self.UID)

    def contains_genuine_serial_number(self) -> bool:
        """Contains a genuine serial number and not an instance ID."""
        return self.uid.contains_genuine_serial_number()

    @property
    def product_id(self) -> str:
        """Returns the product id field."""
        return self.PID or ""

    @property
    def vendor_id(self) -> str:
        """Returns the product id field."""
        return self.VID or ""

    def __eq__(self, other: Any) -> bool:
        """States whether the other id equals to self."""
        if not other or not isinstance(other, UsbIdentifier):
            return False
        if self.is_undefined:
            return other.is_undefined

        return all([self.uid == other.uid, self.product_id == other.product_id, self.vendor_id == other.vendor_id])

    def __hash__(self) -> int:
        """Generates a hash."""
        return hash(self.uid) + hash(self.product_id) + hash(self.vendor_id)

    @property
    def is_undefined(self) -> bool:
        """States whether none of the elements present in DeviceId were defined."""
        return is_undefined_data_object(cast(NamedTuple, self))


class Win32DeviceIdParser:
    """Parser of a standard Win32 device ID.

    See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers
    """

    def parse_uid(self, raw_id: str, serial_number: Optional[str] = None) -> WindowsUID:
        """Parses the UID value.

        As described here:
        https://docs.microsoft.com/it-it/windows-hardware/drivers/install/device-instance-ids
        https://stackoverflow.com/questions/51513337/is-the-usb-instance-id-on-windows-unique-for-a-device
        https://docs.microsoft.com/it-it/windows-hardware/drivers/install/instance-ids
        the instance ID corresponds to the serial number information, if supported by the underlying bus, otherwise
        it is generated by Windows.

        For some boards (e.g. ST boards), the ID may contain other information that we are not interested in
        (e.g. MI value). This method tries to retrieve the actual ID.
        """
        id_elements = raw_id.split("&")
        if len(id_elements) <= 1:
            # The instance ID is the serial number.
            return WindowsUID(uid=raw_id.lower(), raw_uid=raw_id, serial_number=serial_number)
        # The instance ID is generated by Windows and hence might contain other element than the ParentPrefixID.
        # The following tries to only consider what may be the ParentPrefixID.
        return WindowsUID(uid="&".join(id_elements[:-1]).lower(), raw_uid=raw_id, serial_number=serial_number)

    def record_id_element(self, element: str, valuable_information: dict, patterns_dict: dict) -> None:
        """Stores recognised parts of the device ID based on patterns defined."""
        for k, p in patterns_dict.items():
            match = p.fullmatch(element)
            if match:
                valuable_information[k] = match.group(1)

    def split_id_elements(self, parts: List[str], serial_number: str = None) -> dict:
        """Splits the different elements of an Device ID."""
        information = dict()
        information[KEY_UID] = self.parse_uid(parts[-1], serial_number)
        other_elements = parts[-2].split("&")
        patterns_dict = UsbIdentifier.get_patterns_dict()
        for element in other_elements:
            self.record_id_element(element, information, patterns_dict)
        return information

    def parse(self, id_string: Optional[str], serial_number: Optional[str] = None) -> "UsbIdentifier":
        r"""Parses the device id string and retrieves the different elements of interest.

        See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers
         Format: <device-ID>\<instance-specific-ID>
         Ex. `USB\VID_2109&PID_8110\5&376ABA2D&0&21`
          - `<device-ID>`: `USB\VID_2109&PID_8110`
          - `<instance-specific-ID>`: `5&376ABA2D&0&21`
        [Device instance IDs](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/device-instance-ids)
        -> [Device IDs](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/device-ids)
        -> [Hardware IDs](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/hardware-ids)
        -> [Device identifier formats](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/device-identifier-formats)  # noqa: E501
        -> [Identifiers for USB](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifiers-for-usb-devices)
         - [Standard USB Identifiers](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers)
         - [Special USB Identifiers](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/special-usb-identifiers)
          - [Instance specific ID](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/instance-ids)

        Returns:
            corresponding DeviceIdInformation.
        """
        if not id_string or len(id_string.strip()) == 0:
            return UsbIdentifier()
        parts = id_string.split("\\")
        if len(parts) < 2:
            return UsbIdentifier()
        return UsbIdentifier(**self.split_id_elements(parts, serial_number))


def parse_device_id(id_string: Optional[str], serial_number: Optional[str] = None) -> UsbIdentifier:
    """Parses the device id string and retrieves the different elements of interest.

    See https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers
    """
    return Win32DeviceIdParser().parse(id_string, serial_number)
